﻿// $Id: $
// $DateTime: $
// Copyright © 2008 Tristen Fielding

using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;

using Visibility=ResXFileCodeGenerator.Visibility;

namespace ResXFileCodeGenerator.Internal
{
    /// <summary>
    /// Generates source code for resource file wrappers.
    /// </summary>
    internal sealed class CodeGen
    {
        public CodeGen(CodeDomProvider codeProvider, string namespaceName)
            : this(codeProvider, namespaceName, true, true)
        {
            // Empty.
        }

        public CodeGen(CodeDomProvider codeProvider, string namespaceName, bool allowLateBound, bool requireVariableDeclaration)
        {
            if(codeProvider == null)
            {
                throw new ArgumentNullException("codeProvider");
            }

            _codeProvider = codeProvider;

            _code = new CodeCompileUnit();
            _code.UserData.Add("AllowLateBound", allowLateBound);
            _code.UserData.Add("RequireVariableDeclaration", requireVariableDeclaration);

            _codeNamespace = new CodeNamespace(namespaceName);
            _code.Namespaces.Add(_codeNamespace);
        }

        public StreamWriter ToStream()
        {
            StreamWriter streamWriter = new StreamWriter(new MemoryStream(), Encoding.UTF8);
            CodeGeneratorOptions options = new CodeGeneratorOptions
            {
                BlankLinesBetweenMembers = true,
                BracingStyle = "C",
                VerbatimOrder = true
            };

            _codeProvider.GenerateCodeFromCompileUnit(_code, streamWriter, options);

            streamWriter.Flush();
            return streamWriter;
        }

        public void AddReferencedAssembly(string assemblyName)
        {
            _code.ReferencedAssemblies.Add(assemblyName);
        }

        public void AddUsingNamespace(string usingNamespace)
        {
            _codeNamespace.Imports.Add(new CodeNamespaceImport(usingNamespace));
        }

        public CodeTypeDeclaration AddClass(Visibility visibility, Partial makePartial, Sealed makeSealed, string className)
        {
            return AddClass(null, visibility, makePartial, makeSealed, className, false);
        }

        public CodeTypeDeclaration AddInnerClass(CodeTypeDeclaration outerClassDeclaration, Visibility visibility, Partial makePartial, Sealed makeSealed, string className)
        {
            bool makeInnerClass = _codeProvider.Supports(GeneratorSupport.NestedTypes);

            return AddClass(outerClassDeclaration, visibility, makePartial, makeSealed, className, makeInnerClass);
        }

        public void AddSummaryComments(CodeTypeMember typeMember, params string[] summaryComments)
        {
            typeMember.Comments.Add(new CodeCommentStatement(Strings.DocComment_SummaryStart, true));

            foreach (string comment in summaryComments)
            {
                typeMember.Comments.Add(new CodeCommentStatement(comment, true));
            }

            typeMember.Comments.Add(new CodeCommentStatement(Strings.DocComment_SummaryEnd, true));
        }

        public void AddRemarkComments(CodeTypeMember typeMember, params string[] remarkComments)
        {
            if (string.IsNullOrEmpty(remarkComments[0]))
                return;

            typeMember.Comments.Add(new CodeCommentStatement(Strings.DocComment_RemarksStart, true));

            foreach(string comment in remarkComments)
            {
                typeMember.Comments.Add(new CodeCommentStatement(comment, true));
            }

            typeMember.Comments.Add(new CodeCommentStatement(Strings.DocComment_RemarksEnd, true));
        }

        public void AddParamComments(CodeTypeMember typeMember, string paramName, params string[] paramComments)
        {
            if (string.IsNullOrEmpty(paramComments[0]))
                return;

            typeMember.Comments.Add(new CodeCommentStatement(string.Format(CultureInfo.CurrentCulture, 
                Strings.DocComment_ParamStart, paramName), true));

            foreach (string comment in paramComments)
            {
                typeMember.Comments.Add(new CodeCommentStatement(comment, true));
            }

            typeMember.Comments.Add(new CodeCommentStatement(Strings.DocComment_ParamEnd, true));
        }

        public CodeAttributeDeclaration AddGeneratedCodeAttribute(CodeTypeMember typeMember, Type thisType)
        {
            Version assemblyVersion = thisType.Assembly.GetName().Version;

            CodeAttributeArgument stronglyTypedResourceBuilderExAttributeArgument = new CodeAttributeArgument(new CodePrimitiveExpression(thisType.FullName));
            CodeAttributeArgument primitiveExpressionAttributeArgument = new CodeAttributeArgument(new CodePrimitiveExpression(assemblyVersion.ToString()));

            return AddGlobalAttribute(typeMember, typeof(GeneratedCodeAttribute),
                stronglyTypedResourceBuilderExAttributeArgument,
                primitiveExpressionAttributeArgument);
        }

        public CodeAttributeDeclaration AddCustomAttribute(CodeTypeMember typeMember, Type attributeType, params CodeAttributeArgument[] attributeArguments)
        {
            return AddAttribute(typeMember, attributeType, CodeTypeReferenceOptions.GenericTypeParameter, attributeArguments);
        }

        public CodeAttributeDeclaration AddGlobalAttribute(CodeTypeMember typeMember, Type attributeType, params CodeAttributeArgument[] attributeArguments)
        {
            return AddAttribute(typeMember, attributeType, CodeTypeReferenceOptions.GlobalReference, attributeArguments);
        }

        public CodeConstructor AddConstructor(CodeTypeDeclaration classDeclaration, Visibility visibility)
        {
            CodeConstructor codeConstructor = new CodeConstructor
            {
                Attributes = ConvertVisibilityToMemberAttributes(visibility)
            };

            classDeclaration.Members.Add(codeConstructor);

            return codeConstructor;
        }

        public CodeMemberField AddField(CodeTypeDeclaration classDeclaration, Type typeRef, string variableName)
        {
            return AddField(classDeclaration, typeRef, variableName, CodeTypeReferenceOptions.GenericTypeParameter);
        }

        public CodeMemberField AddField(CodeTypeDeclaration classDeclaration, Type typeRef, string variableName, CodeTypeReferenceOptions options)
        {
            return AddField(classDeclaration, typeRef, variableName, options, Static.No);
        }

        public CodeMemberField AddField(CodeTypeDeclaration classDeclaration, Type typeRef, string variableName, CodeTypeReferenceOptions options, Static makeStatic)
        {
            return AddField(classDeclaration, typeRef, variableName, options, makeStatic, Visibility.Private);
        }

        public CodeMemberField AddField(CodeTypeDeclaration classDeclaration, Type typeRef, string variableName, CodeTypeReferenceOptions options, Static makeStatic, Visibility visibility)
        {
            CodeMemberField memberField = new CodeMemberField(new CodeTypeReference(typeRef, options), variableName)
            {
                Attributes = ConvertVisibilityToMemberAttributes(visibility)
            };

            if (CanMakeStatic(makeStatic))
                memberField.Attributes |= MemberAttributes.Static;

            classDeclaration.Members.Add(memberField);

            return memberField;
        }

        public CodeMemberProperty AddProperty(CodeTypeDeclaration classDeclaration, Type typeRef, string propertyName)
        {
            return AddProperty(classDeclaration, typeRef, propertyName, true, true);
        }

        public CodeMemberProperty AddProperty(CodeTypeDeclaration classDeclaration, Type typeRef,
            string propertyName, bool hasGetter, bool hasSetter)
        {
            return AddProperty(classDeclaration, typeRef, propertyName, hasGetter, hasSetter, Visibility.Public);
        }

        public CodeMemberProperty AddProperty(CodeTypeDeclaration classDeclaration, Type typeRef,
            string propertyName, bool hasGetter, bool hasSetter, Visibility visibility)
        {
            return AddProperty(classDeclaration, typeRef, propertyName, hasGetter, hasSetter, visibility, Static.No);
        }

        public CodeMemberProperty AddProperty(CodeTypeDeclaration classDeclaration, Type typeRef,
            string propertyName, bool hasGetter, bool hasSetter, Static makeStatic)
        {
            return AddProperty(classDeclaration, typeRef, propertyName, hasGetter, hasSetter, Visibility.Public, makeStatic,
                CodeTypeReferenceOptions.GenericTypeParameter);
        }

        public CodeMemberProperty AddProperty(CodeTypeDeclaration classDeclaration, Type typeRef,
            string propertyName, bool hasGetter, bool hasSetter, Visibility visibility, Static makeStatic)
        {
           return  AddProperty(classDeclaration, typeRef, propertyName, hasGetter, hasSetter, visibility, makeStatic,
                CodeTypeReferenceOptions.GenericTypeParameter);
        }

        public CodeMemberProperty AddProperty(CodeTypeDeclaration classDeclaration, Type typeRef, string propertyName, bool hasGetter, bool hasSetter, Visibility visibility, Static makeStatic, CodeTypeReferenceOptions options)
        {
            CodeMemberProperty property = new CodeMemberProperty
            {
                Name = propertyName,
                HasGet = hasGetter,
                HasSet = hasSetter,
                Type = new CodeTypeReference(typeRef, options),
                Attributes = ConvertVisibilityToMemberAttributes(visibility)
            };

            if (CanMakeStatic(makeStatic))
                property.Attributes |= MemberAttributes.Static;

            classDeclaration.Members.Add(property);

            return property;
        }

        public CodeMemberMethod AddMethod(CodeTypeDeclaration classDeclaration, Visibility visibility, Static makeStatic, Type returnType, string methodName, params ParamInfo[] paramInfo)
        {
            bool foundParamsKeyword = false;
            CodeMemberMethod method = new CodeMemberMethod
            {
                Name = methodName,
                Attributes = ConvertVisibilityToMemberAttributes(visibility),
                ReturnType = new CodeTypeReference(returnType)
            };

            if (CanMakeStatic(makeStatic))
                method.Attributes = (method.Attributes & ~MemberAttributes.ScopeMask) | MemberAttributes.Static;

            foreach(ParamInfo info in paramInfo)
            {
                if (info.ParamType == typeof(void))
                    throw new ArgumentException("CodeGen.AddMethod: undefined param type for " + info.ParamName);

                CodeParameterDeclarationExpression arg = new CodeParameterDeclarationExpression(info.ParamType, info.ParamName);

                // To add a 'params' keyword we must do the following see: http://blogs.msdn.com/bclteam/archive/2006/04/10/571096.aspx
                if (info.AddParamsKeyword)
                {
                    if (foundParamsKeyword)
                        throw new ArgumentException("CodeGen.AddMethod: only the last parameter can be marked with Params.");

                    arg.CustomAttributes.Add(new CodeAttributeDeclaration("System.ParamArrayAttribute"));
                    foundParamsKeyword = true;
                }

                arg.Direction = info.ParamDirection;
                method.Parameters.Add(arg);
            }
            classDeclaration.Members.Add(method);

            return method;
        }

        public CodeTypeMember AddErrorDirective(CodeTypeDeclaration classDeclaration, string message)
        {
            // The #error directive is only recognized in C# however #error will still cause an error
            // in most (if not all) languages so we will still use it regardless of what language we are generating.
            //
            string msg = "#error - " + message;
            var stm = new CodeSnippetTypeMember(msg);

            classDeclaration.Members.Add(stm);

            return stm;
        }

        public CodeFieldReferenceExpression CreateFieldRef(Type objectType, string variableName)
        {
            if (objectType == null)
                return new CodeFieldReferenceExpression(null, variableName);

            return new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(objectType), variableName);
        }

        public CodeMethodInvokeExpression CreateStaticMethodCall(Type objectType, string methodName)
        {
            if (objectType == null)
            {
                return new CodeMethodInvokeExpression(new CodeMethodReferenceExpression(null, methodName));
            }
            
            CodeTypeReferenceExpression typeRef =
                new CodeTypeReferenceExpression(new CodeTypeReference(objectType));
            return new CodeMethodInvokeExpression(new CodeMethodReferenceExpression(typeRef, methodName));
        }

        public CodeMethodInvokeExpression CreateMethodCall(string variableName, string methodName)
        {
            CodeVariableReferenceExpression varRef = new CodeVariableReferenceExpression(variableName);
            return new CodeMethodInvokeExpression(new CodeMethodReferenceExpression(varRef, methodName));
        }

        public CodePropertyReferenceExpression CreateStaticPropertyCall(Type objectType, string propertyName)
        {
            if (objectType == null)
            {
                return new CodePropertyReferenceExpression(null, propertyName);
            }
            
            CodeTypeReferenceExpression typeRef =
                new CodeTypeReferenceExpression(new CodeTypeReference(objectType));
            return new CodePropertyReferenceExpression(typeRef, propertyName);
        }

        public CodePropertyReferenceExpression CreateStaticPropertyCall(string className, string propertyName)
        {
            if (string.IsNullOrEmpty(className))
            {
                return new CodePropertyReferenceExpression(null, propertyName);
            }
            
            CodeTypeReferenceExpression typeRef =
                new CodeTypeReferenceExpression(new CodeTypeReference(className));
            return new CodePropertyReferenceExpression(typeRef, propertyName);
        }

        public CodePropertyReferenceExpression CreatePropertyCall(string variableName, string propertyName)
        {
            CodeVariableReferenceExpression varRef = new CodeVariableReferenceExpression(variableName);
            return new CodePropertyReferenceExpression(varRef, propertyName);
        }

        private CodeTypeDeclaration AddClass(CodeTypeDeclaration outerClassDeclaration, Visibility visibility, Partial makePartial, Sealed makeSealed, string className, bool nestedClass)
        {
            CodeTypeDeclaration classDeclaration = new CodeTypeDeclaration(className);
            TypeAttributes typeAttributes = ConvertVisibilityToTypeAttributes(visibility, nestedClass);

            classDeclaration.IsClass = true;

            if (CanMakePartial(makePartial))
                classDeclaration.IsPartial = true;

            if (CanMakeSealed(makeSealed))
            {
                typeAttributes |= TypeAttributes.Sealed;
            }

            classDeclaration.TypeAttributes = typeAttributes;

            if (outerClassDeclaration != null)
                outerClassDeclaration.Members.Add(classDeclaration);
            else
                _codeNamespace.Types.Add(classDeclaration);

            return classDeclaration;
        }

        private static CodeAttributeDeclaration AddAttribute(CodeTypeMember typeMember, Type attributeType,
            CodeTypeReferenceOptions options, params CodeAttributeArgument[] attributeArguments)
        {
            CodeAttributeDeclaration attributeDeclaration = new CodeAttributeDeclaration(new CodeTypeReference(attributeType));
            
            attributeDeclaration.AttributeType.Options = options;
            if (attributeArguments != null)
                attributeDeclaration.Arguments.AddRange(attributeArguments);

            typeMember.CustomAttributes.Add(attributeDeclaration);

            return attributeDeclaration;
        }

        private static TypeAttributes ConvertVisibilityToTypeAttributes(Visibility visibility, bool nestedType)
        {
            TypeAttributes attributes = TypeAttributes.Public;

            switch (visibility)
            {
            case Visibility.Public:
                attributes = nestedType ? TypeAttributes.NestedPublic : TypeAttributes.Public;
                break;

            case Visibility.Protected:
                if (nestedType)
                    attributes = TypeAttributes.NestedFamily;
                else
                    throw new ArgumentOutOfRangeException("visibility", "The visibility is not valid here.");
                break;

            case Visibility.Internal:
                attributes = nestedType ? TypeAttributes.NestedAssembly : TypeAttributes.NotPublic;
                break;

            case Visibility.ProtectedInternal:
                if (nestedType)
                    attributes = TypeAttributes.NestedFamORAssem;
                else
                    throw new ArgumentOutOfRangeException("visibility", "The visibility is not valid here.");
                break;

            case Visibility.Private:
                if (nestedType)
                    attributes = TypeAttributes.NestedPrivate;
                else
                    throw new ArgumentOutOfRangeException("visibility", "The visibility is not valid here.");
                break;
            }

            return attributes;
        }

        private static MemberAttributes ConvertVisibilityToMemberAttributes(Visibility visibility)
        {
            MemberAttributes attributes = MemberAttributes.Public;

            switch(visibility)
            {
            case Visibility.Public:
                attributes = MemberAttributes.Public;
                break;

            case Visibility.Protected:
                attributes = MemberAttributes.Family;
                break;

            case Visibility.Internal:
                attributes = MemberAttributes.Assembly;
                break;

            case Visibility.ProtectedInternal:
                attributes = MemberAttributes.FamilyOrAssembly;
                break;

            case Visibility.Private:
                attributes = MemberAttributes.Private;
                break;
            }

            return attributes;
        }

        private bool CanMakeStatic(Static makeStatic)
        {
            return makeStatic == Static.Yes && _codeProvider.Supports(GeneratorSupport.PublicStaticMembers);
        }

        private bool CanMakePartial(Partial makePartial)
        {
            return makePartial == Partial.Yes && _codeProvider.Supports(GeneratorSupport.PartialTypes);
        }

        private static bool CanMakeSealed(Sealed makeSealed)
        {
            return makeSealed == Sealed.Yes;
        }

        #region Fields

        private readonly CodeDomProvider _codeProvider;
        private readonly CodeCompileUnit _code;
        private readonly CodeNamespace _codeNamespace;

        #endregion
    }
}